﻿/*
Copyright (c) 2007-2011 William POTTIER - AllProgrammic

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights 
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 
of the Software, and to permit persons to whom the Software is furnished to do so, 
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all 
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using NetAccess.Common.Exceptions;
using NetAccess.Formatters.BaseFormatters;
using NetAccess.Message;
using System.Threading;
using NetAccess.Common;
#if DEBUG
using System.Collections.Specialized;
using System.Collections;
#endif

namespace NetAccess.Formatters
{
    /// <summary>
    /// Classe prenant en charge la conversion des messages en binaire et leur récupération
    /// </summary>
    public sealed class MessageFormatter : IDisposable
    {
        #region Fields
        private MemoryStream _bufferReceptionStream;
        private BinaryReader _bufferReader;
        private Dictionary<Int32, PartialMessage> _partialMessage;

        private Dictionary<Type, ITypeFormatter> _typeFormatters;
        private Dictionary<Type, BaseTypeEnum> _typeEnum;
        private System.Runtime.Serialization.Formatters.Binary.BinaryFormatter _serializer;

        private readonly Mutex _mutexProcessingReceive = new Mutex(false);

        private Int32 _writeBufferSize;
        #endregion
        
        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="writeBufferSize"></param>
        public MessageFormatter(int writeBufferSize)
        {
            _writeBufferSize = writeBufferSize;
            Initialize();
        }

        private void Initialize()
        {
            _partialMessage = new Dictionary<int, PartialMessage>();
            _typeFormatters = new Dictionary<Type, ITypeFormatter>();

            // Initialize les types formatter de base
            AddTypeFormatter(new BoolFormatter());
            AddTypeFormatter(new ByteFormatter());
            AddTypeFormatter(new CharFormatter());
            AddTypeFormatter(new DecimalFormatter());
            AddTypeFormatter(new DoubleFormatter());
            AddTypeFormatter(new Int16Formatter());
            AddTypeFormatter(new Int32Formatter());
            AddTypeFormatter(new Int64Formatter());
            AddTypeFormatter(new SByteFormatter());
            AddTypeFormatter(new SingleFormatter());
            AddTypeFormatter(new StringFormatter());
            AddTypeFormatter(new UInt16Formatter());
            AddTypeFormatter(new UInt32Formatter());
            AddTypeFormatter(new UInt64Formatter());
            AddTypeFormatter(new NetworkMessageFormatter());

            // Initialize la correspondance des type de base avec l'enum
            _typeEnum = new Dictionary<Type, BaseTypeEnum>
                            {
                {typeof(Boolean),BaseTypeEnum.Bool},
                {typeof(Byte),BaseTypeEnum.Byte},
                {typeof(Char),BaseTypeEnum.Char},
                {typeof(Decimal),BaseTypeEnum.Decimal},
                {typeof(Double),BaseTypeEnum.Double},
                {typeof(Int16),BaseTypeEnum.Int16},
                {typeof(Int32),BaseTypeEnum.Int32},
                {typeof(Int64),BaseTypeEnum.Int64},
                {typeof(SByte),BaseTypeEnum.SByte},
                {typeof(Single),BaseTypeEnum.Single},
                {typeof(String),BaseTypeEnum.String},
                {typeof(UInt16),BaseTypeEnum.UInt16},
                {typeof(UInt32),BaseTypeEnum.UInt32},
                {typeof(UInt64),BaseTypeEnum.UInt64},
            };
        }

        /// <summary>
        /// Add a formatter
        /// </summary>
        /// <param name="formatter">TypeFormatter à ajouter</param>
        public void AddTypeFormatter(ITypeFormatter formatter)
        {
            formatter.RegisterMessageFormatter(this);
            _typeFormatters.Add(formatter.FormatterOfType(), formatter);
        }

        #region Sending

        /// <summary>
        /// Prépare un message pour l'envoi
        /// </summary>
        /// <param name="message">InternalMessage à envoyer</param>
        /// <param name="compression">If true data will be compressed</param>
        /// <param name="publicKey">clef publique pour un éventuel crytage</param>
        /// <returns>File de packet prêt à l'envoi</returns>
        internal Queue<byte[]> Prepare(InternalMessage message, Boolean compression = false, byte[] publicKey = null)
        {
            var result = new Queue<byte[]>();

            // Define if we must encrypt data
            var encrypt = publicKey != null;

            // On formatte les données du message
            var messageStream = new MemoryStream();
            var messageWriter = new BinaryWriter(messageStream);
            messageWriter.Write((Int16)message.MessageIdentifier);
            FormatArray(ref messageWriter, message.MessageData);
            messageWriter.Flush();
            messageStream.Seek(0, SeekOrigin.Begin);
            var messageReader = new BinaryReader(messageStream);

            if (compression)
            {
                // On compresse le memorystream
                throw new NotImplementedException();
            }

            if (encrypt)
            {
                // On crypte le flux
                throw new NotImplementedException();
            }

            // Découpe le contenu
            var numberOfPacket = (Int16)((messageReader.BaseStream.Length / _writeBufferSize) + ((messageReader.BaseStream.Length % _writeBufferSize > 0) ? 1 : 0));

            // Identifiant unique du message
            var messageUID = message.GetHashCode();

            for (Int16 i = 1; i <= numberOfPacket; i++)
            {
                Int32 pktSize = (i < numberOfPacket) ? _writeBufferSize : (Int32)(messageReader.BaseStream.Length - messageReader.BaseStream.Position);
                if (pktSize <= 0 || numberOfPacket <= 0 || i <= 0)
                {
                    throw new ArgumentOutOfRangeException("message", "An error occured : packetSize null or negative detected");
                }

                // Crée le flux du packet
                var pktStream = new MemoryStream(pktSize + 13);
                var pktWriter = new BinaryWriter(pktStream);

                // Ecrit les en-têtes

                // UID du message
                pktWriter.Write(messageUID);
                // Numéro du packet
                pktWriter.Write(i);
                // Nombre de packet
                pktWriter.Write(numberOfPacket);
                // Les données sont cryptée
                pktWriter.Write(encrypt);
                // La taille du bloc de données
                pktWriter.Write(pktSize);

                // Ecrit les données
                pktWriter.Write(messageReader.ReadBytes(pktSize));

                // Flush le buffer
                pktWriter.Flush();

                // Ajoute le packet à la file
                result.Enqueue(pktStream.ToArray());

                // Vide les buffers
                pktWriter.Close();
            }

            messageWriter.Close();
            messageReader.Close();

            Logger.Instance.Debug("Prepare message for sending : " + messageUID + " ("+ numberOfPacket +")");

            return result;
        }
        #endregion

        #region Reception

        /// <summary>
        /// Ajoute un flux de données reçu à la file de traitement
        /// </summary>
        /// <param name="buffer">buffer containing all received data</param>
        /// <param name="numread"></param>
        internal void AddDataToReception(byte[] buffer, int numread)
        {
            _mutexProcessingReceive.WaitOne();
            try
            {

                if (_bufferReceptionStream == null)
                {
                    _bufferReceptionStream = new MemoryStream(numread);
                    _bufferReader = new BinaryReader(_bufferReceptionStream);
                }
                else
                {
                    var currentBuffer = _bufferReceptionStream.ToArray();
                    _bufferReceptionStream = new MemoryStream(currentBuffer.Length + numread);
                    _bufferReceptionStream.Write(currentBuffer, 0, currentBuffer.Length);

                    _bufferReader.Close();
                    _bufferReader = new BinaryReader(_bufferReceptionStream);
                }

                _bufferReceptionStream.Write(buffer, 0, numread);
                _bufferReceptionStream.Flush();

                // Analysis of incomming data
                while (_bufferReceptionStream.Length > 13)
                {
                    _bufferReader.BaseStream.Seek(0, SeekOrigin.Begin);

                    var messageUID = _bufferReader.ReadInt32();
                    var pktID = _bufferReader.ReadInt16();
                    var pktNumber = _bufferReader.ReadInt16();
                    var crypted = _bufferReader.ReadBoolean();
                    var pktSize = _bufferReader.ReadInt32();

                    if (pktSize <= 0 || pktNumber <= 0 || pktID <= 0)
                    {
                        // Packet size error
                        _bufferReceptionStream = new MemoryStream();
                        _bufferReader.Close();
                        _bufferReader = new BinaryReader(_bufferReceptionStream);

                        Logger.Instance.Warning("Receive a message packet with no size, no number or no ID. Destroy current buffer.");

                        break;
                    }

                    // Is there enough data to process?
                    if (_bufferReceptionStream.Length >= (pktSize + _bufferReader.BaseStream.Position))
                    {
                        Logger.Instance.Debug(String.Format("Incomming message : {0} ({1}/{2})", messageUID, pktID, pktNumber));

                        byte[] content = _bufferReader.ReadBytes(pktSize);

                        if (!_partialMessage.ContainsKey(messageUID))
                            _partialMessage.Add(messageUID, new PartialMessage(messageUID, pktNumber, crypted));

                        _partialMessage[messageUID].Packets.Add(pktID, content);

                        // Empty part of buffer
                        var newBuffer = _bufferReader.ReadBytes((Int32)(_bufferReader.BaseStream.Length - _bufferReader.BaseStream.Position));
                        _bufferReceptionStream = new MemoryStream(newBuffer);
                        _bufferReader.Close();
                        _bufferReader = new BinaryReader(_bufferReceptionStream);
                        _bufferReceptionStream.Flush();
                    }
                    else
                    {
                        // Not enough data to continue; Next data will be analize when new data was received
                        break;
                    }

                    // Go to the beginning of the buffer
                    _bufferReceptionStream.Seek(0, SeekOrigin.Begin);
                }
            }
            finally
            {
                _mutexProcessingReceive.ReleaseMutex();
            }
        }

        /// <summary>
        /// Process completed message
        /// </summary>
        /// <returns></returns>
        internal Queue<InternalMessage> ProcessIncommingMessages()
        {
            _mutexProcessingReceive.WaitOne();

            var queue = new Queue<InternalMessage>();

            try
            {
                var processed = new List<int>();

                foreach (var message in _partialMessage.Values)
                {
                    if (message.IsComplete())
                    {
                        queue.Enqueue(ReceiveMessage(message));
                        processed.Add(message.Id);

                        Logger.Instance.Debug("Receive message completed : " + message.Id);
                    }

                    // Delete message too old : probably we never get the complete message
                    if ((DateTime.Now - message.DateBeginReception) < new TimeSpan(0, 5, 0)) continue;

                    processed.Add(message.Id);
                    Logger.Instance.Warning("Delete older not completed message : " + message.Id);
                }

                // ReSharper disable ForCanBeConvertedToForeach
                for (var i = 0; i < processed.Count; i++)
                {
                    _partialMessage.Remove(processed[i]);
                }
                // ReSharper restore ForCanBeConvertedToForeach
            }
            catch (Exception ex)
            {
                Logger.Instance.Error(ex);
            }
            finally
            {
                _mutexProcessingReceive.ReleaseMutex();
            }

            return queue;
        }

        /// <summary>
        /// Recrée les messages
        /// </summary>
        /// <param name="message"></param>
        /// <returns></returns>
        private InternalMessage ReceiveMessage(PartialMessage message)
        {
            MemoryStream messageStream = message.GetBuffer();

            if (message.IsEncrypted)
            {
                // Unencrypt message
                throw new NotImplementedException();
            }

            var messageReader = new BinaryReader(messageStream);

            // Récupère le type du message interne
            var imType = (InternalMessageIdentifier) Enum.ToObject(
                typeof (InternalMessageIdentifier),
                _typeFormatters[typeof (Int16)].UnFormat(ref messageReader));

            // Recrée le message
            return new InternalMessage(imType, UnFormatArray(ref messageReader));
        }
        #endregion

        #region Format
        /// <summary>
        /// Effectue le formatage d'un objet
        /// </summary>
        /// <param name="stream"></param>
        /// <param name="data"></param>
        public void FormatObject(ref BinaryWriter stream, object data)
        {
            if (data == null)
            {
                // Marqueur de type
                stream.Write((Int16)BaseTypeEnum.Null);
            }
            else
            {
                var typeData = data.GetType();

                if (_typeEnum.ContainsKey(typeData))
                {
                    // Marqueur de type
                    stream.Write((Int16)_typeEnum[typeData]);

                    _typeFormatters[typeData].Format(ref stream, data);
                }
                else
                {
                    // On vérifie si il s'agit d'une enum
                    if (data is Enum)
                    {
                        // Marqueur de type
                        stream.Write((Int16)BaseTypeEnum.Enum);

                        // Nom de l'enum
                        stream.Write(TypeName(typeData));

                        // Valeur
                        var underType = Enum.GetUnderlyingType(typeData);
                        _typeFormatters[underType].Format(ref stream, data);
                    }
                    else if (_typeFormatters.ContainsKey(typeData)) // Il ne s'agit pas d'un type de base
                    {
                        // Marqueur de type
                        stream.Write((Int16)BaseTypeEnum.Formatted);

                        // Nom du type (pour le retrouver)
                        stream.Write(TypeName(typeData));

                        // Crée le flux pour le type donnée
                        var objectStream = new MemoryStream();
                        var objectWriter = new BinaryWriter(objectStream);

                        _typeFormatters[typeData].Format(ref objectWriter, data);

                        if (objectStream.Length > Int32.MaxValue)
                            throw new ArgumentException("Impossible de mettre en forme un objet d'une taille supérieur à " + Int32.MaxValue);

                        // Ecrit la taille du flux de l'objet
                        stream.Write((Int32)objectStream.Length);

                        // Ecrit l'objet
                        stream.Write(objectStream.ToArray());

                        // Ferme les flux
                        objectWriter.Close();
                    }
                    else
                    {
                        // Marqueur de type
                        stream.Write((Int16)BaseTypeEnum.Serialized);

                        if (_serializer == null)
                            _serializer = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();

                        using (var ms = new MemoryStream())
                        {
                            try
                            {
                                // On serialize
                                _serializer.Serialize(ms, data);

                                if (ms.Length > Int32.MaxValue)
                                    throw new ArgumentException(
                                        "Impossible de mettre en forme un objet d'une taille supérieur à " +
                                        Int32.MaxValue);

                                // On écrit la taille du flux
                                stream.Write((Int32) ms.Length);

                                // On écrit le flux
                                stream.Write(ms.ToArray());
                            }
                            catch(Exception ex)
                            {
                                Logger.Instance.Error(new NetAccessException("Serialization error. See inner exception for more info.", ex));
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Effectue le formatage d'un tableau
        /// </summary>
        /// <param name="stream"></param>
        /// <param name="data"></param>
        public void FormatArray(ref BinaryWriter stream, object[] data)
        {
            foreach (var t in data)
            {
                FormatObject(ref stream, t);
            }
        }

        #endregion

        #region UnFormat
        /// <summary>
        /// Effectue le deformatage d'un tableau
        /// </summary>
        /// <param name="stream"></param>
        /// <returns></returns>
        public object[] UnFormatArray(ref BinaryReader stream)
        {
            var datas = new List<object>();

            while (stream.BaseStream.Position < stream.BaseStream.Length)
            {
                datas.Add(UnFormatObject(ref stream));
            }

            return datas.ToArray();
        }

        /// <summary>
        /// Effectue le deformatage d'un objet
        /// </summary>
        /// <param name="stream"></param>
        /// <returns></returns>
        public object UnFormatObject(ref BinaryReader stream)
        {
            // Récupère le type de l'objet
            var typeF = (BaseTypeEnum)Enum.ToObject(typeof(BaseTypeEnum), stream.ReadInt16());
            Type dataType = null;

            if (typeF == BaseTypeEnum.Null)
            {
                return null;
            }
            
            if (_typeEnum.ContainsValue(typeF))
            {
                foreach (var kvp in _typeEnum.Where(kvp => kvp.Value == typeF))
                {
                    dataType = kvp.Key;
                    break;
                }

                if(dataType != null) return _typeFormatters[dataType].UnFormat(ref stream);
                throw new NetAccessException("Unable to analyze type.");
            }
            
            switch (typeF)
            {
                case BaseTypeEnum.Serialized:
                    {
                        // Longueur du bloc
                        var size = stream.ReadInt32();

                        // Deserialise la donnée
                        var ms = new MemoryStream();
                        var buffer = stream.ReadBytes(size);
                        ms.Write(buffer, 0, buffer.Length);
                        ms.Seek(0, SeekOrigin.Begin);
                        if (_serializer == null)
                            _serializer = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
                        var data = _serializer.Deserialize(ms);
                        ms.Close();
                        return data;
                    }
                case BaseTypeEnum.Formatted:
                    {
                        var typeString = stream.ReadString();
                        dataType = FindType(typeString);

                        if (dataType != null)
                        {
                            if (_typeFormatters.ContainsKey(dataType))
                            {
                                // Longueur du bloc
                                var size = stream.ReadInt32();

                                var buffer = stream.ReadBytes(size);
                                var objectStream = new MemoryStream(buffer);
                                var objectReader = new BinaryReader(objectStream);

                                return _typeFormatters[dataType].UnFormat(ref objectReader);
                            }
                            
                            throw new FormatterException("Unable to find formatter for type : " + dataType.Name);
                        }
                        
                        throw new FormatterException("Invalid data stream. Unable to deserialize it.");
                    }
                case BaseTypeEnum.Enum:
                    {
                        // Lit le nom de l'enum
                        string typeName = stream.ReadString();

                        // On cherche le type
                        Type typeData;
                        try
                        {
                            typeData = FindType(typeName);
                        }
                        catch(Exception ex)
                        {
                            throw new FormatterException("An error occured during deserialization. See InnerException for more details.", ex);
                        }

                        if (typeData != null && typeData.IsEnum)
                        {
                            var underType = Enum.GetUnderlyingType(typeData);
                            var data = _typeFormatters[underType].UnFormat(ref stream);
                            return Enum.ToObject(typeData, data);
                        }
                        
                        throw new FormatterException("Le flux de données du message est invalide et ne permet pas le déformattage de celui-ci");
                    }
                default:
                    throw new FormatterException("Le flux de données du message est invalide et ne permet pas le déformattage de celui-ci");
            }
        }
        #endregion

        #region Type
        private static string TypeName(Type type)
        {

            if (!type.IsGenericType)
                return type.Name;

            var sb = new StringBuilder(type.Name.Substring(0, type.Name.Length - 2));
            sb.Append('<');
            var first = true;
            foreach (var t in type.GetGenericArguments())
            {
                if (first)
                {
                    sb.Append(',');
                    first = false;
                }
                sb.Append(TypeName(t));
            }
            sb.Append('>');
            return sb.ToString();
        }

        private static Type FindType(string name)
        {
            // ReSharper disable ForCanBeConvertedToForeach
            var assemblies = AppDomain.CurrentDomain.GetAssemblies();

            for (var i = 0; i < assemblies.Length; i++)
            {
                var types = assemblies[i].GetTypes();
                for (var j = 0; j < types.Length; j++)
                {
                    if (name.Equals(TypeName(types[j]), StringComparison.OrdinalIgnoreCase))
                    {
                        return types[j];
                    }
                }
            }

            throw new NetAccessException(new TypeLoadException("Unable to find Type named : " + name));
            // ReSharper restore ForCanBeConvertedToForeach
        }
        #endregion

        #region IDisposable Members
        /// <summary>
        /// Dispose le gestionnaire
        /// </summary>
        public void Dispose()
        {
            if (_bufferReader != null)
            {
                _bufferReader.Close();
                _bufferReader = null;
            }

            if (_bufferReceptionStream != null)
            {
                _bufferReceptionStream.Dispose();
                _bufferReceptionStream = null;
            }

            // ReSharper disable RedundantCheckBeforeAssignment
            if (_serializer != null)
            {
                _serializer = null;
            }
            // ReSharper restore RedundantCheckBeforeAssignment
        }
        #endregion

#if DEBUG
        // Add monitoring&instrumentation in debug mode

        /// <summary>
        /// Event throw when the incomming pool is changing
        /// </summary>
        internal event EventHandler<NotifyCollectionChangedEventArgs> IncommingPoolChange;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="action"></param>
        /// <param name="items"></param>
        private void OnIncommingPoolChange(NotifyCollectionChangedAction action, IList items)
        {
            if(IncommingPoolChange != null)
                IncommingPoolChange(this, new NotifyCollectionChangedEventArgs(action, items));
        }
#endif
    }
}
